<!DOCTYPE html>
<html>
    
<head>
    <meta charset="utf-8">
    
    <title>炫酷！从未见过如此Q弹的Switcher | ApocalypseBlog</title>
    <canvas id="header_canvas"style="position:absolute;bottom:0"></canvas> 
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

    

    

    

    

    
<link rel="stylesheet" href="/dist/build.css?v=1.14.0.css">

    <script src="/javascripts/bubble.js"></script>
    <script>
        window.isPost = true
        window.aomori = {
            
            
        }
        window.aomori_logo_typed_animated = false
        window.aomori_search_algolia = false

    </script>
<script>
    ((window.gitter = {}).chat = {}).options = {
      room: 'ApocalypseBlog/Apocalypse'
    };
  </script>
  <script src="https://sidecar.gitter.im/dist/sidecar.v1.js" async defer></script>
<meta name="generator" content="Hexo 5.3.0"></head>
<!--DaoVoice服务http://blog.daovoice.io/-->

    <script>(function (i, s, o, g, r, a, m) {
        i['DaoVoiceObject'] = r;
        i[r] = i[r] ||
          function () {
            (i[r].q = i[r].q || []).push(arguments);
          };
        i[r].l = 1 * new Date();
        a = s.createElement(o);
        m = s.getElementsByTagName(o)[0];
        a.async = 1;
        a.src = g;
        a.charset = 'utf-8';
        m.parentNode.insertBefore(a, m);
      })(window, document, 'script', ('https:' === document.location.protocol ? 'https:' : 'http:') + "//widget.daovoice.io/widget/b00f5052.js", 'daovoice');
      daovoice('init', {
        app_id: "b00f5052",
      });
      daovoice('update');
    </script>
  
<body>

    <div class="container">
    <header class="header">
        <div class="header-type">
            
            <div class="header-type-inner">
                
                    <a class="header-type-title" href="/">ApocalypseBlog</a>
                
    
                
            </div>
        </div>
        <div class="header-menu">
            <div class="header-menu-inner">
                
            </div>
            <div class="header-menu-social">
                
            </div>
        </div>

        <div class="header-menu-mobile">
            <div class="header-menu-mobile-inner" id="mobile-menu-open">
                <i class="icon icon-menu"></i>
            </div>
        </div>
    </header>

    <div class="header-menu-mobile-menu">
        <div class="header-menu-mobile-menu-bg"></div>
        <div class="header-menu-mobile-menu-wrap">
            <div class="header-menu-mobile-menu-inner">
                <div class="header-menu-mobile-menu-close" id="mobile-menu-close">
                    <i class="icon icon-cross"></i>
                </div>
                <div class="header-menu-mobile-menu-list">
                    
                </div>
            </div>
        </div>
    </div>

</div>

    <div class="container">
        <div class="main">
            <section class="inner">
                <section class="inner-main">
                    <div class="post">
    <article id="post-ckk6o6auz000h7kny5wdmhd6c" class="article article-type-post" itemscope
    itemprop="blogPost">

    <div class="article-inner">

        
          
        
        
        

        
        <header class="article-header">
            
  
    <h1 class="article-title" itemprop="name">
      炫酷！从未见过如此Q弹的Switcher
    </h1>
  

        </header>
        

        <div class="article-more-info article-more-info-post hairline">

            <div class="article-date">
  <time datetime="2020-06-13T18:40:38.000Z" itemprop="datePublished">2020-06-14</time>
</div>

            
            <div class="article-category">
                <a class="article-category-link" href="/categories/Category1/">Category1</a>
            </div>
            

            
            <div class="article-tag">
                <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Tag1/" rel="tag">Tag1</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Tag2/" rel="tag">Tag2</a></li></ul>
            </div>
            

            

        </div>

        <div class="article-entry post-inner-html hairline" itemprop="articleBody">
            <h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>最近逛<code>Dribbble</code>的时候，看到了一个非常酷的<code>Switcher</code>动画，特别喜欢，本想着试着用代码在Android平台来实现一下，没想到已经有实现的版本，并且作者还写了文章分享，思路清晰，各个实现关键点都讲的特别清楚，因此就译诚中文，分享大家，正如作者最后所说，大家一定要运行试试，效果非常赞！</p>
<blockquote>
<p>原作者：<a target="_blank" rel="noopener" href="https://android.jlelse.eu/@bitvale?source=post_page-----433258cbb80f----------------------">Alexander Kolpakov</a> 译者：<a target="_blank" rel="noopener" href="https://www.jianshu.com/u/35167a70aa39">依然范特稀西</a> 地址：<a target="_blank" rel="noopener" href="http://suo.im/60UJjT">http://suo.im/60UJjT</a></p>
</blockquote>
<a id="more"></a>

<h3 id="正文开始"><a href="#正文开始" class="headerlink" title="正文开始"></a>正文开始</h3><p>最近，我写了一篇关于实现Dribbble上的一个漂亮设计的文章。得到了很多积极的反馈，对我来说，这给了我很大的动力。我非常高兴能获得这些反馈，同时我也很乐意分享我的经验。</p>
<p>在本文中，我们将尝试逐步实现由<code>Oleg Frolov</code>创建的另一个精美的动画。这与上一篇文章中的复杂动画UI无关，它是一个自定义小控件。但是它有着非常精美漂亮的动画设计，如下所示：</p>
<p><img src="http://user-gold-cdn.xitu.io/2020/3/12/170cd50c1a6eecc9?imageslim" alt="swicher.gif">swicher.gif</p>
<p>乍一看，实现这样的切换似乎并不简单，但我认为那是因为动画非常漂亮。如 你所见，创建相同的动画并不难。让我们一步一步地来实现它。</p>
<p>第一步，我们需要自定义View,并且实现它的3个构造方法：</p>
<pre><code>class Switcher @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet ? = null,
        defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) &#123;

    init &#123;
        attrs?.let &#123; retrieveAttributes(attrs, defStyleAttr) &#125;
    &#125;

    private fun retrieveAttributes(attrs: AttributeSet, defStyleAttr: Int) &#123;
        // retrieve cutom attributes
    &#125;

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) &#123;
        // setup switcher width and height
    &#125;

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) &#123;
        // setup helper sizes every time switcher size changed (radius, icon width...)
    &#125;

    override fun onDraw(canvas: Canvas ?) &#123;
        // just draw
    &#125;
&#125;
</code></pre>
<h3 id="接下来开始绘制"><a href="#接下来开始绘制" class="headerlink" title="接下来开始绘制"></a>接下来开始绘制</h3><p>在默认（选中）状态下，我们的开关由2个圆角矩形（绿色和白色）组成</p>
<p><img src="http://user-gold-cdn.xitu.io/2020/3/12/170cd50c1f123a19?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>绘制它们非常简单，我们只需要计算白色矩形的位置并将偏移量<code>iconTranslateX</code>传递给Android KTX <code>Canvas.withTranslation</code>扩展即可：</p>
<pre><code>override fun onDraw(canvas: Canvas ?) &#123;
  // draw switcher (green rect)
  canvas?.drawRoundRect(switcherRect, switcherCornerRadius, switcherCornerRadius, switcherPaint)

  // draw icon (white rect)
  canvas?.withTranslation(x = iconTranslateX) &#123;
      drawRoundRect(iconRect, switcherCornerRadius, switcherCornerRadius, iconPaint)
  &#125;
&#125;
</code></pre>
<h3 id="开始分解动画"><a href="#开始分解动画" class="headerlink" title="开始分解动画"></a>开始分解动画</h3><p>动画部分，我们使用<code>ValueAnimator</code>来实现，使用它的<code>ofFloat(float... values)</code>方法,我们需要三个动画：</p>
<ul>
<li>1、<code>switcherAnimator</code>: 切换器图标动画，从白色矩形到圆形，反之亦然</li>
<li>2、<code>translateAnimator</code>: 为切换器图标从左到右的过渡设置动画，反之亦然；</li>
<li>3、<code>colorAnimator</code>: 将颜色从绿色（选中）更改为红色，反之亦然。</li>
</ul>
<p>让我们先看一下<code>switcherAnimator</code>动画，设置<code>0</code>为动画的开始值，<code>1</code>为动画的结束值。</p>
<pre><code>// ...
var amplitude = BOUNCE_ANIM_AMPLITUDE_IN
var frequency = BOUNCE_ANIM_FREQUENCY_IN
var newProgress = 1f

if (!checked) &#123;
    amplitude = BOUNCE_ANIM_AMPLITUDE_OUT
    frequency = BOUNCE_ANIM_FREQUENCY_OUT
    newProgress = 0f
&#125;

val switcherAnimator = ValueAnimator.ofFloat(iconProgress, newProgress).apply &#123;
    addUpdateListener &#123;
        iconProgress = it.animatedValue as Float
    &#125;
    interpolator = BounceInterpolator(amplitude, frequency)
    duration = SWITCHER_ANIMATION_DURATION
&#125;
// ...
</code></pre>
<p>我们可以使用 <a target="_blank" rel="noopener" href="https://github.com/evgenyneu">Evgenii Neumerzhitckii</a> 写的<code>BounceInterpolator</code>来实现反弹效果，这非常适合我们的动画场景，不了解的可以看一下这片文章：<a target="_blank" rel="noopener" href="https://evgenii.com/blog/spring-button-animation-on-android/">evgenii.com/blog/spring…</a>，它解释了<code>BounceInterpolator</code>是如何工作的。</p>
<p>在Android KTX <code>addUpdateListener</code>扩展中，我们更新了<code>progress</code>的值，然后触发<code>invalidate</code>方法，最后重新绘制了视图。</p>
<pre><code>private var iconProgress = 0f
  set(value) &#123;
      if (field != value) &#123;
          field = value

          val iconOffset = lerp(0f, iconRadius - iconCollapsedWidth / 2, value)
          iconRect.left = width - switcherCornerRadius - iconCollapsedWidth / 2 - iconOffset
          iconRect.right = width - switcherCornerRadius + iconCollapsedWidth / 2 + iconOffset

          postInvalidateOnAnimation()
      &#125;
  &#125;
</code></pre>
<p><code>lerp</code>方法类似一个线性插值器，它用于计算<code>iconOffset</code>,反过来，它也用于计算<code>Swicher</code>图标的圆角矩形坐标。此图标的矩形从一个圆角矩形变为一个圆形（圆角半径较大的圆角矩形）。</p>
<blockquote>
<p>我们使用了<code>postInvalidateOnAnimation()</code>代替<code>postIvalidate</code>,是因为我们需要平滑的动画，并且第一个方法有优势，详情请看：<a target="_blank" rel="noopener" href="https://stackoverflow.com/questions/29219372/postinvalidateonanimation-vs-postinvalidate/42648958#42648958">stackoverflow.com/questions/2…</a></p>
</blockquote>
<p><code>translateAnimator</code>的工作方式相同，但是会更新<code>Swicher</code>图标的<code>x</code>位置。</p>
<p><img src="http://user-gold-cdn.xitu.io/2020/3/12/170cd50c209a787a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img"></p>
<p>如你所见，我们的白色圆圈就像百吉饼。我们有2种制作方法：</p>
<ul>
<li>裁剪一个较小的圆圈</li>
<li>最简单的圆圈，只需在顶部绘制另一个小圆圈，然后用切换器颜色填充即可。</li>
</ul>
<p>我选择较简单的一种。</p>
<p>这一切，没什么难的！我们现在有一个漂亮的自定义小控件并且带有精美的动画！</p>
<h3 id="至此，我们一切都可以了"><a href="#至此，我们一切都可以了" class="headerlink" title="至此，我们一切都可以了"></a>至此，我们一切都可以了</h3><p>现在，我们可以使用任何类型的动画来创建自定义视图，而且我们可以稍微更改代码以创建另一个由<code>Oleg Frolov</code>设计的<code>Swicher</code>小部件。仅需将视图轮廓从圆角矩形更新为圆形，并禁用平移动画。就是这么简单。</p>
<p><img src="http://user-gold-cdn.xitu.io/2020/3/12/170cd50c223d523d?imageslim" alt="swicherX.gif">swicherX.gif</p>
<h3 id="Talk-is-Chep，Just-show-Code"><a href="#Talk-is-Chep，Just-show-Code" class="headerlink" title="Talk is Chep，Just show Code"></a>Talk is Chep，Just show Code</h3><p>随意获取GitHub上的源代码，查看我的其他实现，别忘了尝试一下！ Github: <a target="_blank" rel="noopener" href="https://github.com/bitvale/Switcher">github.com/bitvale/Swi…</a></p>

        </div>

    </div>

    

    

    

    

    

    
<nav class="article-nav">
  
    <a href="/2020/06/14/Kotlin%E4%B8%8EJava%E7%9A%84%E5%BC%82%E5%90%8C/" id="article-nav-newer" class="article-nav-link-wrap">
      <div class="article-nav-caption">下一篇</div>
      <div class="article-nav-title">
        
          Kotlin与Java的异同
        
      </div>
    </a>
  
  
    <a href="/2020/06/14/Kotlin%E5%AE%9E%E6%88%98-%E8%AF%AD%E6%B3%95%E7%B3%96/" id="article-nav-older" class="article-nav-link-wrap">
      <div class="article-nav-caption">上一篇</div>
      <div class="article-nav-title">Kotlin实战 | 语法糖</div>
    </a>
  
</nav>


    <section class="share">
        <div class="share-title">分享</div>
        <a class="share-item" target="_blank"
            href="https://twitter.com/share?text=炫酷！从未见过如此Q弹的Switcher - ApocalypseBlog&url=http://example.com/2020/06/14/%E7%82%AB%E9%85%B7%EF%BC%81%E4%BB%8E%E6%9C%AA%E8%A7%81%E8%BF%87%E5%A6%82%E6%AD%A4Q%E5%BC%B9%E7%9A%84Switcher/">
            <box-icon type='logo' name='twitter'></box-icon>
        </a>
        <a class="share-item" target="_blank"
            href="https://www.facebook.com/sharer.php?title=炫酷！从未见过如此Q弹的Switcher - ApocalypseBlog&u=http://example.com/2020/06/14/%E7%82%AB%E9%85%B7%EF%BC%81%E4%BB%8E%E6%9C%AA%E8%A7%81%E8%BF%87%E5%A6%82%E6%AD%A4Q%E5%BC%B9%E7%9A%84Switcher/">
            <box-icon name='facebook-square' type='logo' ></box-icon>
        </a>
        <!-- <a class="share-item" target="_blank"
            href="https://service.weibo.com/share/share.php?title=炫酷！从未见过如此Q弹的Switcher - ApocalypseBlog&url=http://example.com/2020/06/14/%E7%82%AB%E9%85%B7%EF%BC%81%E4%BB%8E%E6%9C%AA%E8%A7%81%E8%BF%87%E5%A6%82%E6%AD%A4Q%E5%BC%B9%E7%9A%84Switcher/&pic=">
            <div class="n-icon n-icon-weibo"></div>
        </a> -->
    </section>

</article>









</div>
                </section>
            </section>

             
            <aside class="sidebar">
            
                
            </aside>
        </div>
    </div>

    <footer class="footer">
    <div class="footer-wave">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="#3c4859" fill-opacity="1" d="M0,160L60,181.3C120,203,240,245,360,240C480,235,600,181,720,186.7C840,192,960,256,1080,261.3C1200,267,1320,213,1380,186.7L1440,160L1440,320L1380,320C1320,320,1200,320,1080,320C960,320,840,320,720,320C600,320,480,320,360,320C240,320,120,320,60,320L0,320Z"></path></svg>
    </div>

    <div class="footer-wrap">
        <div class="footer-inner"> 
            ApocalypseBlog &copy; 2021<br>
            Powered By Hexo · Theme By <a href="https://github.com/lh1me/hexo-theme-aomori" target="_blank">Aomori</a>
        </div>
    </div>

</footer>




<script src="/dist/build.js?1.14.0.js"></script>


<script src="/dist/custom.js?1.14.0.js"></script>









</body>

</html>